P09-2. Grafana Loki와 서비스 메시 로그 아키텍처 - 실습 검증

Part 09 (실습): LGTM 로그 파이프라인 — Grafana Loki와 서비스 메시 로그 아키텍처 검증

실습 목표

  1. monitoring 네임스페이스에 클라우드 네이티브 로그 저장소인 Grafana Loki (3.4.2) 를 배포하고, Grafana Alloy 에 쿠버네티스 파드 로그 수집 파이프라인(loki.source.kubernetes → loki.write)을 연결한다.
  2. Istio Telemetry API 리소스를 적용해 파드 재시작 없이 Waypoint Proxy의 L7 Envoy 액세스 로그를 실시간으로 활성화한다.
  3. Waypoint가 남긴 L7 HTTP 액세스 로그와 외부 PostgreSQL 18 VM(172.19.0.7:5432)으로 향하는 TCP 액세스 로그의 각 컬럼 구조를 해부한다.
  4. Loki의 /loki/api/v1/query_range API에 직접 LogQL 쿼리를 날려 part1-demo의 L7 로그와 istio-system(ztunnel)의 L4 HBONE 로그를 초고속으로 검색·검증한다.

사전 환경 확인

  • P09-1 이론 개념을 먼저 읽고 오면 왜 Loki가 라벨만 색인하는지, 그리고 Envoy 응답 플래그(UO, UT, UF)와 LogQL 문법이 어떻게 동작하는지 명확히 이해됩니다.
  • Part 08 실습에서 구축한 monitoring 네임스페이스(alloy, vmsingle) 위에서 바로 이어서 진행합니다.
  • Windows PowerShell 창을 열고 아래 명령어로 상태를 확인합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl get pods -n monitoring
    


실습 아키텍처 및 로그 수집 파이프라인 한눈에 보기

flowchart LR
    subgraph LOG_SOURCES["로그 발생원 (Kubernetes Pods)"]
        ZT["ztunnel (istio-system)
L4 HBONE 연결 로그 (logfmt)
src.identity / dst.hbone_addr"] WP["Waypoint (part1-demo)
Telemetry API로 활성화!
L7 Envoy HTTP & TCP 액세스 로그"] APP["order-api (part1-demo)
애플리케이션 컨테이너 로그"] end subgraph ALLOY_LOKI["monitoring 네임스페이스"] ALLOY["Grafana Alloy (v1.21.2)
1. discovery.relabel (namespace/pod/app)
2. loki.source.kubernetes
3. loki.write"] LOKI["Grafana Loki (3.4.2)
:3100 (/loki/api/v1/push & /query_range)
라벨 색인 + 본문 고압축 청크 저장"] end ZT -->|"Pod Log Stream"| ALLOY WP -->|"Pod Log Stream"| ALLOY APP -->|"Pod Log Stream"| ALLOY ALLOY ==>|"Loki Push"| LOKI

Step 1. Grafana Loki (3.4.2) 배포 및 Grafana Alloy 로그 파이프라인 확장

먼저 monitoring 네임스페이스에 loki (grafana/loki:3.4.2) 를 배포하고, alloy-config ConfigMap에 part1-demo와 istio-system 네임스페이스의 파드 로그를 실시간으로 수집해 Loki로 밀어 넣는 loki.source.kubernetes 블록을 추가합니다.
동시에 part1-demo 네임스페이스에 Istio kind: Telemetry 리소스를 적용해 Waypoint Proxy의 Envoy 액세스 로그 출력을 켭니다!

# part9-loki-stack.yaml 생성 및 적용
@"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: loki
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: loki
  template:
    metadata:
      labels:
        app: loki
    spec:
      containers:
      - name: loki
        image: grafana/loki:3.4.2
        args:
        - "-config.file=/etc/loki/local-config.yaml"
        ports:
        - containerPort: 3100
---
apiVersion: v1
kind: Service
metadata:
  name: loki
  namespace: monitoring
spec:
  selector:
    app: loki
  ports:
  - name: http
    port: 3100
    targetPort: 3100
---
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
  name: mesh-observability
  namespace: part1-demo
spec:
  accessLogging:
  - providers:
    - name: envoy
"@ | Set-Content -Path "$HOME\mesh-study\part9-loki-stack.yaml" -Encoding utf8

kubectl apply -f "$HOME\mesh-study\part9-loki-stack.yaml"
kubectl wait --for=condition=Ready pod -l app=loki -n monitoring --timeout=120s

이어서 Grafana Alloy 설정(alloy-config)에 아래 loki.source.kubernetes 파이프라인이 포함되도록 업데이트하고 Alloy를 재시작합니다(Part 10의 Tempo OTLP 설정까지 한 번에 포함된 통합 설정입니다):

    // [L] Kubernetes 파드 로그 수집 (part1-demo 및 istio-system) -> Loki 전송
    discovery.relabel "mesh_logs" {
      targets = discovery.kubernetes.pods.targets
      rule {
        source_labels = ["__meta_kubernetes_namespace"]
        regex         = "part1-demo|istio-system"
        action        = "keep"
      }
      rule {
        source_labels = ["__meta_kubernetes_namespace"]
        target_label  = "namespace"
      }
      rule {
        source_labels = ["__meta_kubernetes_pod_name"]
        target_label  = "pod"
      }
      rule {
        source_labels = ["__meta_kubernetes_pod_container_name"]
        target_label  = "container"
      }
      rule {
        source_labels = ["__meta_kubernetes_pod_label_app"]
        target_label  = "app"
      }
    }

    loki.source.kubernetes "mesh_logs" {
      targets    = discovery.relabel.mesh_logs.output
      forward_to = [loki.write.loki.receiver]
    }

    loki.write "loki" {
      endpoint {
        url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
      }
    }

Step 2. Telemetry API로 활성화된 Waypoint Envoy 액세스 로그 실물 해부!

이제 study-client에서 추적하기 쉬운 고유 요청 번호 헤더(-H "x-request-id: 99999999-aaaa-bbbb-cccc-1234567890ab")를 붙여 order-api를 3번 호출한 뒤, Waypoint Proxy 파드가 출력한 Envoy 액세스 로그를 직접 열어봅니다!

# 1. 고유 x-request-id 헤더를 포함하여 3회 요청 전송
kubectl exec -n part1-demo study-client -- curl -s -H "x-request-id: 99999999-aaaa-bbbb-cccc-1234567890ab" "http://order-api/trace-and-log-check?[1-3]" | Out-Null

# 2. Waypoint Proxy 파드의 실시간 Envoy 액세스 로그 확인!
kubectl logs -n part1-demo -l gateway.networking.k8s.io/gateway-name=waypoint --tail=5

실제 출력 결과:

2026-10-25T07:38:21.161606Z	info	Envoy proxy is ready
[2026-10-25T07:38:23.338Z] "- - -" 0 - - - "-" 3709 6990 83 - "-" "-" "-" "-" "172.19.0.7:5432" inbound-vip|5432|tcp|pg-vm.mesh.internal 10.244.2.20:36068 240.240.0.50:5432 10.244.2.14:39092 - -
[2026-10-25T07:38:24.430Z] "GET /trace-and-log-check?1 HTTP/1.1" 200 - via_upstream - "-" 0 616 13 10 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.6:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
[2026-10-25T07:38:24.452Z] "GET /trace-and-log-check?2 HTTP/1.1" 200 - via_upstream - "-" 0 616 9 8 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.6:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
[2026-10-25T07:38:24.463Z] "GET /trace-and-log-check?3 HTTP/1.1" 200 - via_upstream - "-" 0 619 10 8 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.10:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
Waypoint 로그에 찍힌 두 종류의 핵심 기록을 해부해 봅시다!

1. 첫 번째 줄 (외부 PostgreSQL 18 VM으로 나간 L4 TCP 로그):

  • "- - -" 0 - ... 3709 6990 83 ... "172.19.0.7:5432" inbound-vip|5432|tcp|pg-vm.mesh.internal ... 240.240.0.50:5432
  • postgres-exporter 파드가 가상 IP(240.240.0.50:5432)로 접속해 외부 Linux VM(172.19.0.7:5432)과 3,709 바이트를 보내고 6,990 바이트를 받으며 83ms 동안 통신한 기록이 Waypoint에 정확히 남았습니다!

2. 두 번째~네 번째 줄 (order-api로 들어간 L7 HTTP 로그):

  • "GET /trace-and-log-check?1 HTTP/1.1": HTTP 메서드, 경로, 프로토콜
  • 200 - via_upstream: HTTP 상태 코드(200), Envoy 응답 플래그(- = 정상), 응답 출처(via_upstream = 백엔드 파드가 정상 응답함; 만약 서킷 브레이커가 터지면 여기가 503 UO upstream_reset_before_response_started{overflow}로 찍힙니다!)
  • 0 616 13 10: 받은 본문 크기(0B), 보낸 응답 크기(616B), 전체 소요 시간(13ms), 순수 백엔드 파드 처리 시간(10ms) (즉, 프록시 네트워크 오버헤드는 단 3ms!)
  • "99999999-aaaa-bbbb-cccc-1234567890ab": 요청의 고유 추적 ID (x-request-id)
  • "envoy://connect_originate/10.244.2.6:8080": Waypoint가 이 요청을 HBONE(CONNECT) 터널에 실어 10.244.2.6:8080 파드로 보냈다는 뜻!

Step 3. Grafana Loki API에 LogQL 쿼리를 날려 실시간 검색 검증!

이제 Grafana Alloy가 위 로그들을 Grafana Loki 저장소(:3100)로 잘 밀어 넣었는지, Loki의 /loki/api/v1/query_range API에 직접 LogQL 쿼리를 날려 확인합니다!

PowerShell에서 kubectl exec -- curl로 Loki 쿼리를 날릴 때의 팁

PowerShell은 작은따옴표 안의 큰따옴표({namespace="part1-demo"})를 외부 프로그램(kubectl)에 넘길 때 벗겨내는 성질이 있습니다. 따라서 URL에 %22(큰따옴표)로 인코딩하거나 Grafana 화면에서 조회하면 따옴표 유실 없이 100% 정확하게 동작합니다.

# 1. [LogQL 쿼리 1] {namespace="part1-demo"} |= "/trace-and-log-check" (Waypoint L7 로그 검색!)
kubectl exec -n part1-demo study-client -- curl -s -g "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22part1-demo%22%7D%7C%3D%22/trace-and-log-check%22&limit=2"

# 2. [LogQL 쿼리 2] {namespace="istio-system"} |= "connection complete" (ztunnel L4 HBONE 로그 검색!)
kubectl exec -n part1-demo study-client -- curl -s -g "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22istio-system%22%7D%7C%3D%22connection%20complete%22&limit=2"

실제 출력 결과 (핵심 JSON 발췌):

// [1. part1-demo의 Waypoint L7 Envoy 액세스 로그 검색 결과 (execTime: 0.18초!)]
{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [
      {
        "stream": {
          "container": "istio-proxy",
          "job": "loki.source.kubernetes.mesh_logs",
          "namespace": "part1-demo",
          "pod": "waypoint-5c968455-ml6h7",
          "service_name": "istio-proxy"
        },
        "values": [
          [
            "1792913905356707846",
            "[2026-10-25T07:38:24.463Z] \"GET /trace-and-log-check?3 HTTP/1.1\" 200 - via_upstream - \"-\" 0 619 10 8 \"-\" \"curl/8.10.1\" \"99999999-aaaa-bbbb-cccc-1234567890ab\" \"order-api\" \"envoy://connect_originate/10.244.2.10:8080\" ...\n"
          ]
        ]
      }
    ]
  }
}

// [2. istio-system의 ztunnel L4 HBONE 액세스 로그 검색 결과 (execTime: 0.029초!)]
{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [
      {
        "stream": {
          "app": "ztunnel",
          "container": "istio-proxy",
          "job": "loki.source.kubernetes.mesh_logs",
          "namespace": "istio-system",
          "pod": "ztunnel-zb2gf",
          "service_name": "ztunnel"
        },
        "values": [
          [
            "1792913993667655162",
            "2026-10-25T07:39:53.657461Z\tinfo\taccess\tconnection complete\tsrc.addr=10.244.2.7:38102 src.workload=\"study-client\" src.namespace=\"part1-demo\" src.identity=\"spiffe://cluster.local/ns/part1-demo/sa/default\" ... dst.addr=10.244.2.17:15008 dst.hbone_addr=10.244.2.17:3100 dst.service=\"loki.monitoring.svc.cluster.local\" ...\n"
          ]
        ]
      }
    ]
  }
}

Step 4. 자주 발생하는 문제와 해결 (Troubleshooting)

증상 / 에러 메시지 원인 해결 방법
Waypoint 파드 로그(kubectl logs)에 아무런 HTTP 요청 기록이 안 찍힘 Istio는 성능 최적화를 위해 기본적으로 Envoy 액세스 로그를 꺼 둠 Step 1처럼 part1-demo(또는 istio-system 전역)에 kind: Telemetry (accessLogging.providers: [{name: envoy}]) 리소스를 적용
PowerShell에서 curl ... 'query={namespace="part1-demo"}' 실행 시 syntax error: unexpected IDENTIFIER, expecting STRING 발생 PowerShell이 외부 프로세스 호출 시 문자열 내부의 큰따옴표(")를 제거해 {namespace=part1-demo}로 전달함 큰따옴표 자리에 URL 인코딩(%22)을 쓰거나 \"로 이스케이프 처리
Alloy 로그에 pods/log is forbidden: User "system:serviceaccount:monitoring:alloy" cannot list resource "pods/log" 에러 Alloy 서비스 계정의 ClusterRole에 pods/log 리소스 권한이 누락됨 ClusterRole(alloy-observer)의 resources 배열에 "pods/log"를 반드시 포함

Step 5. 이번 파트 핵심 명령어 정리 (Command Reference)

# 1. Istio Telemetry 리소스 확인 (액세스 로깅 및 트레이싱 활성화 상태)
kubectl get telemetry -n part1-demo -o yaml

# 2. Waypoint Proxy가 실시간으로 찍는 Envoy 액세스 로그 직접 확인
kubectl logs -n part1-demo -l gateway.networking.k8s.io/gateway-name=waypoint --tail=10

# 3. Grafana Loki에 저장된 로그 라벨(Stream Labels) 종류 확인
kubectl exec -n part1-demo study-client -- curl -s "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/labels"

Step 6. 셀프 체크 (Self-Check Q&A)

Q1. Grafana Loki가 Elasticsearch보다 메모리를 훨씬 적게 먹으면서도 빠르게 동작하는 비결은 무엇이며, 라벨을 설계할 때 무엇을 주의해야 하나요?

Q2. Envoy(Waypoint) 액세스 로그에서 응답 코드 옆에 찍히는 UO, UT, UF 플래그는 각각 무슨 뜻인가요?


🔗 다음 파트로 이동

이제 M(Metric) 과 L(Log) 두 기둥이 모두 세워졌습니다!
다음 Part 10에서는 세 번째 기둥인 T(Grafana Tempo) 를 통해 Waypoint가 자동으로 찍어주는 W3C Trace Context (traceparent) 헤더와 OpenTelemetry(OTLP :4317) 분산 트레이스를 직접 추적해 봅니다!


네비게이션

이전 목록 다음
◀ Part 09 이론 목록으로 Part 10 이론 ▶